Skip to content

Conversation

@mikenomitch
Copy link
Collaborator

Adds support for custom tokens in exposePort. - Allows for consistent URLs across sessions

Allows users to pass a custom token to exposePort() to maintain
consistent preview URLs across deployments and container restarts.
This solves the issue of URLs breaking when sandboxes are restarted
during deployments, enabling stable URL sharing in production.

Token validation ensures 16-character length and URL-safe characters
(lowercase letters, numbers, hyphens, underscores) matching the
existing routing pattern. Backward compatible - tokens are optional
and auto-generated if not provided.

Includes comprehensive unit tests for validation logic and custom
token usage.
Update token validation to accept variable-length tokens (1-63 chars)
instead of fixed 16-character requirement. This provides more flexibility
for users to choose meaningful token names while maintaining DNS
subdomain compatibility.

Changes:
- Updated validateCustomToken() to check 1-63 char range
- Updated request-handler regex to accept variable-length tokens
- Updated all documentation and JSDoc comments
- Updated unit tests to cover variable-length tokens
- Updated changeset with new token length requirements
@changeset-bot
Copy link

changeset-bot bot commented Jan 3, 2026

🦋 Changeset detected

Latest commit: 334ce49

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@cloudflare/sandbox Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

agents-git-bot bot pushed a commit to cloudflare/cloudflare-docs that referenced this pull request Jan 3, 2026
Add documentation for the new custom token parameter in exposePort() that enables stable preview URLs across container restarts and deployments.

Updated files:
- API reference (ports.mdx): Added token parameter documentation with examples
- Concepts (preview-urls.mdx): Updated URL format and added URL stability section
- Guide (expose-services.mdx): Added practical examples and use case guidance

Related PR: cloudflare/sandbox-sdk#329
@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 3, 2026

Open in StackBlitz

npm i https://pkg.pr.new/cloudflare/sandbox-sdk/@cloudflare/sandbox@329

commit: 334ce49

@github-actions
Copy link
Contributor

github-actions bot commented Jan 3, 2026

🐳 Docker Images Published

Default:

FROM cloudflare/sandbox:0.0.0-pr-329-92798c8

With Python:

FROM cloudflare/sandbox:0.0.0-pr-329-92798c8-python

With OpenCode:

FROM cloudflare/sandbox:0.0.0-pr-329-92798c8-opencode

Version: 0.0.0-pr-329-92798c8

Use the -python variant if you need Python code execution, or -opencode for the variant with OpenCode AI coding agent pre-installed.


📦 Standalone Binary

For arbitrary Dockerfiles:

COPY --from=cloudflare/sandbox:0.0.0-pr-329-92798c8 /container-server/sandbox /sandbox
ENTRYPOINT ["/sandbox"]

Download via GitHub CLI:

gh run download 20972927899 -n sandbox-binary

Extract from Docker:

docker run --rm cloudflare/sandbox:0.0.0-pr-329-92798c8 cat /container-server/sandbox > sandbox && chmod +x sandbox

Copy link
Collaborator

@whoiskatrin whoiskatrin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Copy link
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

Critical Issue: Validation Ordering Bug

Location: packages/sandbox/src/sandbox.ts:2133-2151

Token validation happens after the container API call. Invalid tokens are sent to the container before validation runs:

// Line 2133: Container called FIRST
await this.client.ports.exposePort(
  port,
  sessionId,
  options?.name,
  options?.token  // Could be invalid
);

// Line 2151: Validation SECOND (too late)
if (options.token) {
  this.validateCustomToken(options.token);
  token = options.token;
}

If a user passes token: "INVALID@#$", it reaches the container unchecked. While validation eventually throws, the container has already processed the request.

Move validation before the container call:

// Validate token FIRST
let token: string;
if (options.token) {
  this.validateCustomToken(options.token);
  token = options.token;
} else {
  token = this.generatePortToken();
}

const sessionId = await this.ensureDefaultSession();
await this.client.ports.exposePort(
  port,
  sessionId,
  options?.name,
  options.token
);

Missing Test Coverage

Three critical integration scenarios aren't tested:

  1. 63-character boundary: Tests reject 64 chars but don't verify 63 chars works (packages/sandbox/tests/sandbox.test.ts:293)
  2. Token storage: No test verifies custom tokens are stored correctly, unlike auto-generated tokens (line 340-346)
  3. Request routing: All request-handler tests use 16-char tokens; none test the regex change from {16} to + with variable lengths (packages/sandbox/tests/request-handler.test.ts)

Add tests that verify custom tokens flow through correctly, not just that bad tokens are rejected.

Verdict

Fix the validation ordering before merge. The test gaps should be addressed but aren't blocking.

let token: string;
if (options.token) {
// Validate custom token
this.validateCustomToken(options.token);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validation happens after the container API call at line 2133. Move the entire token generation/validation block (lines 2147-2155) before the client.ports.exposePort() call to validate before side effects.

Update custom token validation to enforce a maximum of 16 characters
to match the auto-generated token length and ensure consistency.

Changes:
- Updated validateCustomToken() to check 1-16 char range (was 1-63)
- Updated all JSDoc comments to reflect 1-16 char requirement
- Updated interface documentation in ExposePortRequest
- Updated unit tests to verify 16-char maximum
- Updated changeset with corrected token length
Copy link
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

The implementation is well-structured with good validation logic and test coverage. However, there's one critical security issue that must be addressed before merging.

Critical Issue: Token Collision

Security Risk (10/10): The code doesn't prevent multiple ports from using the same custom token. This creates a security vulnerability where users could potentially access wrong ports or hijack preview URLs.

// Currently allowed - creates security issue:
await sandbox.exposePort(8080, { hostname: 'example.com', token: 'mytoken' });
await sandbox.exposePort(8081, { hostname: 'example.com', token: 'mytoken' });
// Both URLs use same token - which port gets accessed?

Required fix: Before storing a custom token (sandbox.ts:2157), check if it's already in use by another port:

if (options.token) {
  const existingTokens = tokens || {};
  const existingPort = Object.entries(existingTokens).find(
    ([p, t]) => t === options.token && p !== port.toString()
  );
  if (existingPort) {
    throw new SecurityError(
      `Token '${options.token}' is already in use by port ${existingPort[0]}.`
    );
  }
  token = options.token;
}

Required test: Add to sandbox.test.ts:

it('should reject exposing different ports with same custom token', async () => {
  await sandbox.exposePort(8080, { hostname: 'example.com', token: 'shared' });
  await expect(
    sandbox.exposePort(8081, { hostname: 'example.com', token: 'shared' })
  ).rejects.toThrow(/already in use/);
});

Other Issues

Token persistence testing: Missing E2E test for token survival across DO hibernation. This is critical for production stability - if tokens don't persist, preview URLs break after sleep/wake cycles.

Regex edge cases: The URL pattern regex needs tests for consecutive separators (abc--def, test__123) to ensure they parse correctly.

What Looks Good

  • Comprehensive input validation with clear error messages
  • Proper SecurityError usage
  • Type consistency across SDK and shared packages
  • Good unit test coverage for validation rules
  • Well-documented with JSDoc and examples
  • Changeset follows project standards

Verdict: Fix the token collision issue, then this is ready to merge.

token = this.generatePortToken();
}

// Store token for this port (storage is protected by input gates)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security issue: Need to check if the custom token is already in use by another port before storing. Currently allows token collision which could let users access wrong ports.

Add before line 2157:

if (options.token) {
  const existingPort = Object.entries(tokens).find(
    ([p, t]) => t === options.token && p !== port.toString()
  );
  if (existingPort) {
    throw new SecurityError(
      `Token '${options.token}' is already in use by port ${existingPort[0]}.`
    );
  }
}

function extractSandboxRoute(url: URL): RouteInfo | null {
// Parse subdomain pattern: port-sandboxId-token.domain (tokens mandatory)
// Token is always exactly 16 chars (generated by generatePortToken)
// Token can be any length (1-63 chars) containing only [a-z0-9_-]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment says "1-63 chars" but validateCustomToken limits to 16 chars. The regex accepts up to 63 to be forward-compatible with DNS limits, which is fine, but the comment could be clearer: "Token can be 1-16 chars (SDK validates) or up to 63 chars (DNS limit)"

})
);
});
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing critical test case: token collision prevention. Add:

it('should reject exposing different ports with same custom token', async () => {
  await sandbox.exposePort(8080, { hostname: 'example.com', token: 'shared' });
  await expect(
    sandbox.exposePort(8081, { hostname: 'example.com', token: 'shared' })
  ).rejects.toThrow(/already in use/);
});

…prevention

Critical fixes:
- Move token validation before container API call to prevent invalid
  tokens from reaching the container
- Add token collision check to prevent multiple ports using same token
- This fixes security vulnerability where wrong ports could be accessed

Additional improvements:
- Add test for token collision prevention
- Add test for custom token storage verification
- Add test for re-exposing same port with same token (valid scenario)
- Update comment in request-handler.ts for clarity on token length

All feedback from Claude Code Review has been addressed.
@mikenomitch mikenomitch enabled auto-merge (squash) January 9, 2026 23:17
Empty string tokens were bypassing validation because the truthiness
check treated '' as falsy. Changed to explicit undefined check.

Also removed token from container API since the container never used
it, consolidated verbose unit tests into focused ones, and added E2E
test coverage for custom token port exposure.
Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenCode Review

This is a well-implemented feature that adds custom token support for stable preview URLs. The implementation is thoughtful with proper validation, collision detection, and security considerations.

Strengths:

  • Proper input validation with helpful error messages
  • Token collision prevention across ports
  • Good test coverage for edge cases
  • Maintains backward compatibility
  • URL parsing updated to handle variable token lengths
  • Excellent changeset with clear examples

Minor Issues to Address:

  1. Type inconsistency: ExposePortRequest type moved from port-client.ts to shared/request-types.ts but doesn't include the new token field. The container layer won't receive custom tokens.

  2. Potential race condition: Between lines 2172-2181, there's a gap where another request could claim the same custom token. Consider checking collision after storage read and before container call.

  3. Documentation: Request handler comment still mentions "exactly 16 chars" but should reflect "1-16 chars" for custom tokens.

Overall this looks good to merge once the type definition is updated to include the token field in the container API.

Verdict: A few small issues to address first.

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenCode Review

After reviewing the implementation, this is a well-architected feature that adds custom token support for stable preview URLs.

Strengths:

  • Proper input validation with helpful error messages
  • Token collision prevention across ports
  • Good test coverage for edge cases
  • Maintains backward compatibility
  • URL parsing updated to handle variable token lengths
  • Excellent changeset with clear examples
  • Clean separation of concerns: DO handles tokens/URLs, container handles port exposure
  • Documentation updated correctly in request handler

Architecture is correct: The token field doesn't need to be in ExposePortRequest because the container layer only manages port exposure, while the Durable Object layer handles token-based URL access control. This separation is appropriate.

Looks good to merge.

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenCode Review

Looks good overall! The custom token feature for stable preview URLs is well-implemented with proper validation and collision detection.

A few issues to address:

  1. Missing ExposePortRequest field: The interface was moved to @repo/shared but doesn't include the token field, creating a type mismatch between SDK and container
  2. Documentation clarity: The changeset mentions tokens work "across deployments" but they're stored in DO storage which is ephemeral

The architecture follows established patterns and the security considerations (collision detection, input validation) are solid.

// With custom token - URL stays the same across restarts
const { url } = await sandbox.exposePort(8080, {
hostname: 'example.com',
token: 'my-token-v1' // 1-16 chars: a-z, 0-9, -, _
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description mentions "stable preview URLs across deployments" but tokens are stored in Durable Object storage, which is ephemeral and cleared when the sandbox sleeps/restarts. This could mislead users about persistence guarantees.

Consider clarifying: "stable preview URLs within a session" or "while the sandbox remains active" to set accurate expectations.

The standard === operator can leak token contents through timing
differences. While not practically exploitable over the network,
using crypto.subtle.timingSafeEqual follows Cloudflare's recommended
pattern for comparing secrets.
Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenCode Review

Looks good! This is a clean implementation that adds the ability to use custom tokens in exposePort() for stable preview URLs across deployments.

The implementation handles all the important edge cases - token validation, collision detection, and maintains backward compatibility. The timing-safe comparison and input gate protection for storage are correctly implemented. The changeset properly describes the user-facing benefit.

One thing to note: this will require documentation updates since the Cloudflare docs currently show the exposePort() method without the new token parameter. The API reference at /sandbox/api/ports/ and the usage examples in /sandbox/guides/expose-services/ should be updated to document the new optional parameter.

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenCode Review

This PR adds custom token support to exposePort() for stable preview URLs across deployments. The implementation looks solid with proper validation and security considerations.

Good architectural choices:

  • Token collision prevention across ports while allowing re-exposure of same port
  • Timing-safe token comparison using crypto.subtle.timingSafeEqual()
  • Clear validation with descriptive error messages
  • Comprehensive test coverage including edge cases

Security implementation is correct:

  • Validates token format (1-16 chars, lowercase a-z, 0-9, -, _)
  • Prevents token reuse across different ports
  • Uses constant-time comparison to prevent timing attacks

The regex change in request-handler.ts makes sense - relaxing from exactly 16 chars to 1+ chars while maintaining DNS compatibility.

Documentation impact: This change should be documented in the Cloudflare docs since it affects the public API and enables new use cases for production deployments.

Looks good to merge.

@ghostwriternr ghostwriternr merged commit fc1a8ea into main Jan 13, 2026
13 checks passed
@ghostwriternr ghostwriternr deleted the feat/custom-port-tokens branch January 13, 2026 21:30
@github-actions github-actions bot mentioned this pull request Jan 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants